2 // ShareTableViewDataSource.swift
5 // Created by Claudio Cambra on 27/2/24.
11 import NextcloudFileProviderKit
12 import NextcloudCapabilitiesKit
15 class ShareTableViewDataSource: NSObject, NSTableViewDataSource, NSTableViewDelegate {
16 private let shareItemViewIdentifier = NSUserInterfaceItemIdentifier("ShareTableItemView")
17 private let shareItemViewNib = NSNib(nibNamed: "ShareTableItemView", bundle: nil)
18 private let reattemptInterval: TimeInterval = 3.0
20 let kit = NextcloudKit.shared
22 var uiDelegate: ShareViewDataSourceUIDelegate?
23 var sharesTableView: NSTableView? {
25 sharesTableView?.register(shareItemViewNib, forIdentifier: shareItemViewIdentifier)
26 sharesTableView?.rowHeight = 42.0 // Height of view in ShareTableItemView XIB
27 sharesTableView?.dataSource = self
28 sharesTableView?.delegate = self
29 sharesTableView?.reloadData()
32 var capabilities: Capabilities?
34 private(set) var itemURL: URL?
35 private(set) var itemServerRelativePath: String?
36 private(set) var shares: [NKShare] = [] {
37 didSet { Task { @MainActor in sharesTableView?.reloadData() } }
39 private(set) var userAgent: String = "Nextcloud-macOS/FileProviderUIExt"
40 private(set) var account: Account? {
42 guard let account = account else { return }
44 account: account.ncKitAccount,
45 urlBase: account.serverUrl,
46 user: account.username,
47 userId: account.username,
48 password: account.password,
56 func loadItem(url: URL) {
57 itemServerRelativePath = nil
65 DispatchQueue.main.async {
66 Timer.scheduledTimer(withTimeInterval: self.reattemptInterval, repeats: false) { _ in
67 Task { await self.reload() }
73 guard let itemURL else {
74 presentError("No item URL, cannot reload data!")
77 guard let itemIdentifier = await withCheckedContinuation({
78 (continuation: CheckedContinuation<NSFileProviderItemIdentifier?, Never>) -> Void in
79 NSFileProviderManager.getIdentifierForUserVisibleFile(
81 ) { identifier, domainIdentifier, error in
82 defer { continuation.resume(returning: identifier) }
83 guard error == nil else {
84 self.presentError("No item with identifier: \(error.debugDescription)")
89 presentError("Could not get identifier for item, no shares can be acquired.")
94 let connection = try await serviceConnection(url: itemURL, interruptionHandler: {
95 Logger.sharesDataSource.error("Service connection interrupted")
97 guard let serverPath = await connection.itemServerPath(identifier: itemIdentifier),
98 let credentials = await connection.credentials() as? Dictionary<String, String>,
99 let convertedAccount = Account(dictionary: credentials),
100 !convertedAccount.password.isEmpty
102 presentError("Failed to get details from File Provider Extension. Retrying.")
106 let serverPathString = serverPath as String
107 itemServerRelativePath = serverPathString
108 account = convertedAccount
109 await sharesTableView?.deselectAll(self)
110 capabilities = await fetchCapabilities()
111 guard capabilities != nil else { return }
112 guard capabilities?.filesSharing?.apiEnabled == true else {
113 presentError("Server does not support shares.")
116 guard let account else {
117 presentError("Account data is unavailable, cannot reload data!")
120 guard let itemMetadata = await fetchItemMetadata(
121 itemRelativePath: serverPathString, account: account, kit: kit
123 presentError("Unable to retrieve file metadata...")
126 guard itemMetadata.permissions.contains("R") == true else {
127 presentError("This file cannot be shared.")
130 shares = await fetch(
131 itemIdentifier: itemIdentifier, itemRelativePath: serverPathString
133 shares.append(Self.generateInternalShare(for: itemMetadata))
135 presentError("Could not reload data: \(error), will try again.")
141 itemIdentifier: NSFileProviderItemIdentifier, itemRelativePath: String
142 ) async -> [NKShare] {
143 Task { @MainActor in uiDelegate?.fetchStarted() }
144 defer { Task { @MainActor in uiDelegate?.fetchFinished() } }
146 let rawIdentifier = itemIdentifier.rawValue
147 Logger.sharesDataSource.info("Fetching shares for item \(rawIdentifier, privacy: .public)")
149 guard let account else {
150 self.presentError("NextcloudKit instance or account is unavailable, cannot fetch shares!")
154 let parameter = NKShareParameter(path: itemRelativePath)
156 return await withCheckedContinuation { continuation in
158 parameters: parameter, account: account.ncKitAccount
159 ) { account, shares, data, error in
160 let shareCount = shares?.count ?? 0
161 Logger.sharesDataSource.info("Received \(shareCount, privacy: .public) shares")
162 defer { continuation.resume(returning: shares ?? []) }
163 guard error == .success else {
164 self.presentError("Error fetching shares: \(error.errorDescription)")
171 private static func generateInternalShare(for file: NKFile) -> NKShare {
172 let internalShare = NKShare()
173 internalShare.shareType = NKShare.ShareType.internalLink.rawValue
174 internalShare.url = file.urlBase + "/index.php/f/" + file.fileId
175 internalShare.account = file.account
176 internalShare.displaynameOwner = file.ownerDisplayName
177 internalShare.displaynameFileOwner = file.ownerDisplayName
178 internalShare.path = file.path
182 private func fetchCapabilities() async -> Capabilities? {
183 guard let account else {
184 self.presentError("Could not fetch capabilities as account is invalid.")
188 return await withCheckedContinuation { continuation in
189 kit.getCapabilities(account: account.ncKitAccount) { account, data, error in
190 guard error == .success, let capabilitiesJson = data?.data else {
191 self.presentError("Error getting server caps: \(error.errorDescription)")
192 continuation.resume(returning: nil)
195 Logger.sharesDataSource.info("Successfully retrieved server share capabilities")
196 continuation.resume(returning: Capabilities(data: capabilitiesJson))
201 private func presentError(_ errorString: String) {
202 Logger.sharesDataSource.error("\(errorString, privacy: .public)")
203 Task { @MainActor in self.uiDelegate?.showError(errorString) }
206 // MARK: - NSTableViewDataSource protocol methods
208 @objc func numberOfRows(in tableView: NSTableView) -> Int {
212 // MARK: - NSTableViewDelegate protocol methods
214 @objc func tableView(
215 _ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int
217 let share = shares[row]
218 guard let view = tableView.makeView(
219 withIdentifier: shareItemViewIdentifier, owner: self
220 ) as? ShareTableItemView else {
221 Logger.sharesDataSource.error("Acquired item view from table is not a share item view!")
228 @objc func tableViewSelectionDidChange(_ notification: Notification) {
229 guard let selectedRow = sharesTableView?.selectedRow, selectedRow >= 0 else {
230 Task { @MainActor in uiDelegate?.hideOptions(self) }
233 let share = shares[selectedRow]
234 Task { @MainActor in uiDelegate?.showOptions(share: share) }